import { EventEmitter } from "events";

type EventMap<T> = Record<keyof T, any[]> | DefaultEventMap;
type DefaultEventMap = [never];
type AnyRest = [...args: any[]];
type Args<K, T> = T extends DefaultEventMap ? AnyRest : (
    K extends keyof T ? T[K] : never
);
type Key<K, T> = T extends DefaultEventMap ? string | symbol : K | keyof T;
type Key2<K, T> = T extends DefaultEventMap ? string | symbol : K & keyof T;
type Listener<K, T, F> = T extends DefaultEventMap ? F : (
    K extends keyof T ? (
        T[K] extends unknown[] ? (...args: T[K]) => void : never
    )
    : never
);
type Listener1<K, T> = Listener<K, T, (...args: any[]) => void>;
type Listener2<K, T> = Listener<K, T, Function>;

export class ExEventEmitter<T extends EventMap<T>> {
    private eventEmitter = new EventEmitter();

    /**
     * Alias for `emitter.on(eventName, listener)`.
     * @since v0.1.26
     */
    addListener<K>(eventName: Key<K, T>, listener: Listener1<K, T>): this
    addListener<K>(eventName: Key<K, T>, subEventName: string, listener: Listener1<K, T>): this
    addListener<K>(eventName: Key<K, T>, arg1?: string | Listener1<K, T>, arg2?: Listener1<K, T>): this {
        if (typeof arg1 === 'string') {
            this.eventEmitter.on(`${eventName}/${arg1}`, arg2!);
        } else {
            this.eventEmitter.on(eventName as string, arg1!);
        }
        return this;
    }
    /**
     * Adds the `listener` function to the end of the listeners array for the event
     * named `eventName`. No checks are made to see if the `listener` has already
     * been added. Multiple calls passing the same combination of `eventName` and
     * `listener` will result in the `listener` being added, and called, multiple times.
     *
     * ```js
     * server.on('connection', (stream) => {
     *   console.log('someone connected!');
     * });
     * ```
     *
     * Returns a reference to the `EventEmitter`, so that calls can be chained.
     *
     * By default, event listeners are invoked in the order they are added. The `emitter.prependListener()` method can be used as an alternative to add the
     * event listener to the beginning of the listeners array.
     *
     * ```js
     * import { EventEmitter } from 'node:events';
     * const myEE = new EventEmitter();
     * myEE.on('foo', () => console.log('a'));
     * myEE.prependListener('foo', () => console.log('b'));
     * myEE.emit('foo');
     * // Prints:
     * //   b
     * //   a
     * ```
     * @since v0.1.101
     * @param eventName The name of the event.
     * @param listener The callback function
     */
    on<K>(eventName: Key<K, T>, listener: Listener1<K, T>): this
    on<K>(eventName: Key<K, T>, subEventName: string, listener: Listener1<K, T>): this
    on<K>(eventName: Key<K, T>, arg1?: string | Listener1<K, T>, arg2?: Listener1<K, T>): this {
        if (typeof arg1 === 'string') {
            this.eventEmitter.on(`${eventName}/${arg1}`, arg2!);
        } else {
            this.eventEmitter.on(eventName as string, arg1!);
        }
        return this;
    }

    /**
     * Adds a **one-time** `listener` function for the event named `eventName`. The
     * next time `eventName` is triggered, this listener is removed and then invoked.
     *
     * ```js
     * server.once('connection', (stream) => {
     *   console.log('Ah, we have our first user!');
     * });
     * ```
     *
     * Returns a reference to the `EventEmitter`, so that calls can be chained.
     *
     * By default, event listeners are invoked in the order they are added. The `emitter.prependOnceListener()` method can be used as an alternative to add the
     * event listener to the beginning of the listeners array.
     *
     * ```js
     * import { EventEmitter } from 'node:events';
     * const myEE = new EventEmitter();
     * myEE.once('foo', () => console.log('a'));
     * myEE.prependOnceListener('foo', () => console.log('b'));
     * myEE.emit('foo');
     * // Prints:
     * //   b
     * //   a
     * ```
     * @since v0.3.0
     * @param eventName The name of the event.
     * @param listener The callback function
     */
    once<K>(eventName: Key<K, T>, listener: Listener1<K, T>): this
    once<K>(eventName: Key<K, T>, subEventName: string, listener: Listener1<K, T>): this
    once<K>(eventName: Key<K, T>, arg1?: string | Listener1<K, T>, arg2?: Listener1<K, T>): this {
        if (typeof arg1 === 'string') {
            this.eventEmitter.once(`${eventName}/${arg1}`, arg2!);
        } else {
            this.eventEmitter.once(eventName as string, arg1!);
        }
        return this;
    }
    /**
     * Removes the specified `listener` from the listener array for the event named `eventName`.
     *
     * ```js
     * const callback = (stream) => {
     *   console.log('someone connected!');
     * };
     * server.on('connection', callback);
     * // ...
     * server.removeListener('connection', callback);
     * ```
     *
     * `removeListener()` will remove, at most, one instance of a listener from the
     * listener array. If any single listener has been added multiple times to the
     * listener array for the specified `eventName`, then `removeListener()` must be
     * called multiple times to remove each instance.
     *
     * Once an event is emitted, all listeners attached to it at the
     * time of emitting are called in order. This implies that any `removeListener()` or `removeAllListeners()` calls _after_ emitting and _before_ the last listener finishes execution
     * will not remove them from`emit()` in progress. Subsequent events behave as expected.
     *
     * ```js
     * import { EventEmitter } from 'node:events';
     * class MyEmitter extends EventEmitter {}
     * const myEmitter = new MyEmitter();
     *
     * const callbackA = () => {
     *   console.log('A');
     *   myEmitter.removeListener('event', callbackB);
     * };
     *
     * const callbackB = () => {
     *   console.log('B');
     * };
     *
     * myEmitter.on('event', callbackA);
     *
     * myEmitter.on('event', callbackB);
     *
     * // callbackA removes listener callbackB but it will still be called.
     * // Internal listener array at time of emit [callbackA, callbackB]
     * myEmitter.emit('event');
     * // Prints:
     * //   A
     * //   B
     *
     * // callbackB is now removed.
     * // Internal listener array [callbackA]
     * myEmitter.emit('event');
     * // Prints:
     * //   A
     * ```
     *
     * Because listeners are managed using an internal array, calling this will
     * change the position indices of any listener registered _after_ the listener
     * being removed. This will not impact the order in which listeners are called,
     * but it means that any copies of the listener array as returned by
     * the `emitter.listeners()` method will need to be recreated.
     *
     * When a single function has been added as a handler multiple times for a single
     * event (as in the example below), `removeListener()` will remove the most
     * recently added instance. In the example the `once('ping')` listener is removed:
     *
     * ```js
     * import { EventEmitter } from 'node:events';
     * const ee = new EventEmitter();
     *
     * function pong() {
     *   console.log('pong');
     * }
     *
     * ee.on('ping', pong);
     * ee.once('ping', pong);
     * ee.removeListener('ping', pong);
     *
     * ee.emit('ping');
     * ee.emit('ping');
     * ```
     *
     * Returns a reference to the `EventEmitter`, so that calls can be chained.
     * @since v0.1.26
     */
    removeListener<K>(eventName: Key<K, T>, listener: Listener1<K, T>): this
    removeListener<K>(eventName: Key<K, T>, subEventName: string, listener: Listener1<K, T>): this
    removeListener<K>(eventName: Key<K, T>, arg1?: string | Listener1<K, T>, arg2?: Listener1<K, T>): this {
        if (typeof arg1 === 'string') {
            this.eventEmitter.removeListener(`${eventName}/${arg1}`, arg2!);
        } else {
            this.eventEmitter.removeListener(eventName as string, arg1!);
        }
        return this;
    }
    /**
     * Alias for `emitter.removeListener()`.
     * @since v10.0.0
     */
    off<K>(eventName: Key<K, T>, listener: Listener1<K, T>): this
    off<K>(eventName: Key<K, T>, subEventName: string, listener: Listener1<K, T>): this
    off<K>(eventName: Key<K, T>, arg1?: string | Listener1<K, T>, arg2?: Listener1<K, T>): this {
        if (typeof arg1 === 'string') {
            this.eventEmitter.off(`${eventName}/${arg1}`, arg2!);
        } else {
            this.eventEmitter.off(eventName as string, arg1!);
        }
        return this;
    }
    /**
     * Removes all listeners, or those of the specified `eventName`.
     *
     * It is bad practice to remove listeners added elsewhere in the code,
     * particularly when the `EventEmitter` instance was created by some other
     * component or module (e.g. sockets or file streams).
     *
     * Returns a reference to the `EventEmitter`, so that calls can be chained.
     * @since v0.1.26
     */
    removeAllListeners(eventName?: Key<unknown, T>): this
    removeAllListeners(eventName: Key<unknown, T>, subEventName: string): this
    removeAllListeners(eventName?: Key<unknown, T>, subEventName?: string): this {
        if (eventName && subEventName) {
            this.eventEmitter.removeAllListeners(`${eventName}/${subEventName}`);
        } else if (eventName) {
            this.eventEmitter.removeAllListeners(eventName as string);
        } else {
            this.eventEmitter.removeAllListeners();
        }
        return this;
    }
    /**
     * By default `EventEmitter`s will print a warning if more than `10` listeners are
     * added for a particular event. This is a useful default that helps finding
     * memory leaks. The `emitter.setMaxListeners()` method allows the limit to be
     * modified for this specific `EventEmitter` instance. The value can be set to `Infinity` (or `0`) to indicate an unlimited number of listeners.
     *
     * Returns a reference to the `EventEmitter`, so that calls can be chained.
     * @since v0.3.5
     */
    setMaxListeners(n: number): this {
        this.eventEmitter.setMaxListeners(n);
        return this;
    }

    /**
     * Returns the current max listener value for the `EventEmitter` which is either
     * set by `emitter.setMaxListeners(n)` or defaults to {@link defaultMaxListeners}.
     * @since v1.0.0
     */
    getMaxListeners(): number {
        return this.eventEmitter.getMaxListeners();
    }

    /**
     * Returns a copy of the array of listeners for the event named `eventName`.
     *
     * ```js
     * server.on('connection', (stream) => {
     *   console.log('someone connected!');
     * });
     * console.log(util.inspect(server.listeners('connection')));
     * // Prints: [ [Function] ]
     * ```
     * @since v0.1.26
     */
    listeners<K>(eventName: Key<K, T>): Array<Listener2<K, T>>
    listeners<K>(eventName: Key<K, T>, subEventName: string): Array<Listener2<K, T>>
    listeners<K>(eventName: Key<K, T>, subEventName?: string): Array<Listener2<K, T>> {
        if (subEventName) {
            return this.eventEmitter.listeners(`${eventName}/${subEventName}`) as Array<Listener2<K, T>>;
        } else {
            return this.eventEmitter.listeners(eventName as string) as Array<Listener2<K, T>>;
        }
    }
    /**
     * Returns a copy of the array of listeners for the event named `eventName`,
     * including any wrappers (such as those created by `.once()`).
     *
     * ```js
     * import { EventEmitter } from 'node:events';
     * const emitter = new EventEmitter();
     * emitter.once('log', () => console.log('log once'));
     *
     * // Returns a new Array with a function `onceWrapper` which has a property
     * // `listener` which contains the original listener bound above
     * const listeners = emitter.rawListeners('log');
     * const logFnWrapper = listeners[0];
     *
     * // Logs "log once" to the console and does not unbind the `once` event
     * logFnWrapper.listener();
     *
     * // Logs "log once" to the console and removes the listener
     * logFnWrapper();
     *
     * emitter.on('log', () => console.log('log persistently'));
     * // Will return a new Array with a single function bound by `.on()` above
     * const newListeners = emitter.rawListeners('log');
     *
     * // Logs "log persistently" twice
     * newListeners[0]();
     * emitter.emit('log');
     * ```
     * @since v9.4.0
     */
    rawListeners<K>(eventName: Key<K, T>): Array<Listener2<K, T>>
    rawListeners<K>(eventName: Key<K, T>, subEventName: string): Array<Listener2<K, T>>
    rawListeners<K>(eventName: Key<K, T>, subEventName?: string): Array<Listener2<K, T>> {
        if (subEventName) {
            return this.eventEmitter.rawListeners(`${eventName}/${subEventName}`) as Array<Listener2<K, T>>;
        } else {
            return this.eventEmitter.rawListeners(eventName as string) as Array<Listener2<K, T>>;
        }
    }
    /**
     * Synchronously calls each of the listeners registered for the event named `eventName`, in the order they were registered, passing the supplied arguments
     * to each.
     *
     * Returns `true` if the event had listeners, `false` otherwise.
     *
     * ```js
     * import { EventEmitter } from 'node:events';
     * const myEmitter = new EventEmitter();
     *
     * // First listener
     * myEmitter.on('event', function firstListener() {
     *   console.log('Helloooo! first listener');
     * });
     * // Second listener
     * myEmitter.on('event', function secondListener(arg1, arg2) {
     *   console.log(`event with parameters ${arg1}, ${arg2} in second listener`);
     * });
     * // Third listener
     * myEmitter.on('event', function thirdListener(...args) {
     *   const parameters = args.join(', ');
     *   console.log(`event with parameters ${parameters} in third listener`);
     * });
     *
     * console.log(myEmitter.listeners('event'));
     *
     * myEmitter.emit('event', 1, 2, 3, 4, 5);
     *
     * // Prints:
     * // [
     * //   [Function: firstListener],
     * //   [Function: secondListener],
     * //   [Function: thirdListener]
     * // ]
     * // Helloooo! first listener
     * // event with parameters 1, 2 in second listener
     * // event with parameters 1, 2, 3, 4, 5 in third listener
     * ```
     * @since v0.1.26
     */
    emit<K>(eventName: Key<K, T>, ...args: Args<K, T>): boolean {
        return this.eventEmitter.emit(eventName as string, ...args);
    }

    /**
     * Same as `emitter.emit()` but calls each of the listeners registered for the event named `eventName` and `subEventName`.
     * in the order they were registered, passing the supplied arguments to each.
     * @returns 
     */
    emitSub<K>(eventName: Key<K, T>, subEventName: string, ...args: Args<K, T>): boolean {
        return this.eventEmitter.emit(`${eventName}/${subEventName}`, ...args);
    }

    /**
     * Returns the number of listeners listening for the event named `eventName`.
     * If `listener` is provided, it will return how many times the listener is found
     * in the list of the listeners of the event.
     * @since v3.2.0
     * @param eventName The name of the event being listened for
     * @param subEventName The name of the sub event.
     * @param listener The event handler function
     */
    listenerCount<K>(eventName: Key<K, T>, listener?: Listener2<K, T>): number
    listenerCount<K>(eventName: Key<K, T>, subEventName: string, listener?: Listener2<K, T>): number
    listenerCount<K>(eventName: Key<K, T>, arg1?: string | Listener2<K, T>, arg2?: Listener2<K, T>): number {
        if (typeof arg1 === 'string') {
            return this.eventEmitter.listenerCount(`${eventName}/${arg1}`, arg2!);
        } else {
            return this.eventEmitter.listenerCount(eventName as string, arg1!);
        }
    }
    /**
     * Adds the `listener` function to the _beginning_ of the listeners array for the
     * event named `eventName`. No checks are made to see if the `listener` has
     * already been added. Multiple calls passing the same combination of `eventName`
     * and `listener` will result in the `listener` being added, and called, multiple times.
     *
     * ```js
     * server.prependListener('connection', (stream) => {
     *   console.log('someone connected!');
     * });
     * ```
     *
     * Returns a reference to the `EventEmitter`, so that calls can be chained.
     * @since v6.0.0
     * @param eventName The name of the event.
     * @param subEventName The name of the sub event.
     * @param listener The callback function
     */
    prependListener<K>(eventName: Key<K, T>, listener: Listener1<K, T>): this
    prependListener<K>(eventName: Key<K, T>, subEventName: string, listener: Listener1<K, T>): this
    prependListener<K>(eventName: Key<K, T>, arg1?: string | Listener1<K, T>, arg2?: Listener1<K, T>): this {
        if (typeof arg1 === 'string') {
            this.eventEmitter.prependListener(`${eventName}/${arg1}`, arg2!);
        } else {
            this.eventEmitter.prependListener(eventName as string, arg1!);
        }
        return this;
    }

    /**
     * Adds a **one-time**`listener` function for the event named `eventName` to the _beginning_ of the listeners array. The next time `eventName` is triggered, this
     * listener is removed, and then invoked.
     *
     * ```js
     * server.prependOnceListener('connection', (stream) => {
     *   console.log('Ah, we have our first user!');
     * });
     * ```
     *
     * Returns a reference to the `EventEmitter`, so that calls can be chained.
     * @since v6.0.0
     * @param eventName The name of the event.
     * @param subEventName The name of the sub event.
     * @param listener The callback function
     */
    prependOnceListener<K>(eventName: Key<K, T>, listener: Listener1<K, T>): this
    prependOnceListener<K>(eventName: Key<K, T>, subEventName: string, listener: Listener1<K, T>): this
    prependOnceListener<K>(eventName: Key<K, T>, arg1?: string | Listener1<K, T>, arg2?: Listener1<K, T>): this {
        if (typeof arg1 === 'string') {
            this.eventEmitter.prependOnceListener(`${eventName}/${arg1}`, arg2!);
        } else {
            this.eventEmitter.prependOnceListener(eventName as string, arg1!);
        }
        return this;
    }

    /**
     * Returns an array listing the events for which the emitter has registered
     * listeners. The values in the array are strings or `Symbol`s.
     *
     * ```js
     * import { EventEmitter } from 'node:events';
     *
     * const myEE = new EventEmitter();
     * myEE.on('foo', () => {});
     * myEE.on('bar', () => {});
     *
     * const sym = Symbol('symbol');
     * myEE.on(sym, () => {});
     *
     * console.log(myEE.eventNames());
     * // Prints: [ 'foo', 'bar', Symbol(symbol) ]
     * ```
     * @since v6.0.0
     */
    eventNames(): Array<(string | symbol) & Key2<unknown, T>> {
        return this.eventEmitter.eventNames() as Array<(string | symbol) & Key2<unknown, T>>;
    }
}